home *** CD-ROM | disk | FTP | other *** search
/ PC go! 2018 January / PCgo 01-2018 CD-ROM Germany.iso / nw.pak / Unnamed File 000131.txt < prev    next >
Encoding:
Text File  |  2015-07-29  |  43.5 KB  |  1,368 lines

  1. // Copyright (c) 2012 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4.  
  5. // require: array_data_model.js
  6. // require: list_selection_model.js
  7. // require: list_selection_controller.js
  8. // require: list_item.js
  9.  
  10. /**
  11.  * @fileoverview This implements a list control.
  12.  */
  13.  
  14. cr.define('cr.ui', function() {
  15.   /** @const */ var ListSelectionModel = cr.ui.ListSelectionModel;
  16.   /** @const */ var ListSelectionController = cr.ui.ListSelectionController;
  17.   /** @const */ var ArrayDataModel = cr.ui.ArrayDataModel;
  18.  
  19.   /**
  20.    * Whether a mouse event is inside the element viewport. This will return
  21.    * false if the mouseevent was generated over a border or a scrollbar.
  22.    * @param {!HTMLElement} el The element to test the event with.
  23.    * @param {!Event} e The mouse event.
  24.    * @return {boolean} Whether the mouse event was inside the viewport.
  25.    */
  26.   function inViewport(el, e) {
  27.     var rect = el.getBoundingClientRect();
  28.     var x = e.clientX;
  29.     var y = e.clientY;
  30.     return x >= rect.left + el.clientLeft &&
  31.            x < rect.left + el.clientLeft + el.clientWidth &&
  32.            y >= rect.top + el.clientTop &&
  33.            y < rect.top + el.clientTop + el.clientHeight;
  34.   }
  35.  
  36.   function getComputedStyle(el) {
  37.     return el.ownerDocument.defaultView.getComputedStyle(el);
  38.   }
  39.  
  40.   /**
  41.    * Creates a new list element.
  42.    * @param {Object=} opt_propertyBag Optional properties.
  43.    * @constructor
  44.    * @extends {HTMLUListElement}
  45.    */
  46.   var List = cr.ui.define('list');
  47.  
  48.   List.prototype = {
  49.     __proto__: HTMLUListElement.prototype,
  50.  
  51.     /**
  52.      * Measured size of list items. This is lazily calculated the first time it
  53.      * is needed. Note that lead item is allowed to have a different height, to
  54.      * accommodate lists where a single item at a time can be expanded to show
  55.      * more detail.
  56.      * @type {?{height: number, marginTop: number, marginBottom: number,
  57.      *     width: number, marginLeft: number, marginRight: number}}
  58.      * @private
  59.      */
  60.     measured_: null,
  61.  
  62.     /**
  63.      * Whether or not the list is autoexpanding. If true, the list resizes
  64.      * its height to accomadate all children.
  65.      * @type {boolean}
  66.      * @private
  67.      */
  68.     autoExpands_: false,
  69.  
  70.     /**
  71.      * Whether or not the rows on list have various heights. If true, all the
  72.      * rows have the same fixed height. Otherwise, each row resizes its height
  73.      * to accommodate all contents.
  74.      * @type {boolean}
  75.      * @private
  76.      */
  77.     fixedHeight_: true,
  78.  
  79.     /**
  80.      * Whether or not the list view has a blank space below the last row.
  81.      * @type {boolean}
  82.      * @private
  83.      */
  84.     remainingSpace_: true,
  85.  
  86.     /**
  87.      * Function used to create grid items.
  88.      * @type {function(new:cr.ui.ListItem, *)}
  89.      * @private
  90.      */
  91.     itemConstructor_: cr.ui.ListItem,
  92.  
  93.     /**
  94.      * Function used to create grid items.
  95.      * @return {function(new:cr.ui.ListItem, *)}
  96.      */
  97.     get itemConstructor() {
  98.       return this.itemConstructor_;
  99.     },
  100.     set itemConstructor(func) {
  101.       if (func != this.itemConstructor_) {
  102.         this.itemConstructor_ = func;
  103.         this.cachedItems_ = {};
  104.         this.redraw();
  105.       }
  106.     },
  107.  
  108.     dataModel_: null,
  109.  
  110.     /**
  111.      * The data model driving the list.
  112.      * @type {ArrayDataModel}
  113.      */
  114.     set dataModel(dataModel) {
  115.       if (this.dataModel_ == dataModel)
  116.         return;
  117.  
  118.       if (!this.boundHandleDataModelPermuted_) {
  119.         this.boundHandleDataModelPermuted_ =
  120.             this.handleDataModelPermuted_.bind(this);
  121.         this.boundHandleDataModelChange_ =
  122.             this.handleDataModelChange_.bind(this);
  123.       }
  124.  
  125.       if (this.dataModel_) {
  126.         this.dataModel_.removeEventListener(
  127.             'permuted',
  128.             this.boundHandleDataModelPermuted_);
  129.         this.dataModel_.removeEventListener('change',
  130.                                             this.boundHandleDataModelChange_);
  131.       }
  132.  
  133.       this.dataModel_ = dataModel;
  134.  
  135.       this.cachedItems_ = {};
  136.       this.cachedItemHeights_ = {};
  137.       this.selectionModel.clear();
  138.       if (dataModel)
  139.         this.selectionModel.adjustLength(dataModel.length);
  140.  
  141.       if (this.dataModel_) {
  142.         this.dataModel_.addEventListener(
  143.             'permuted',
  144.             this.boundHandleDataModelPermuted_);
  145.         this.dataModel_.addEventListener('change',
  146.                                          this.boundHandleDataModelChange_);
  147.       }
  148.  
  149.       this.redraw();
  150.       this.onSetDataModelComplete();
  151.     },
  152.  
  153.     get dataModel() {
  154.       return this.dataModel_;
  155.     },
  156.  
  157.     /**
  158.      * Override to be notified when |this.dataModel| is set.
  159.      * @protected
  160.      */
  161.     onSetDataModelComplete: function() {
  162.     },
  163.  
  164.     /**
  165.      * Cached item for measuring the default item size by measureItem().
  166.      * @type {cr.ui.ListItem}
  167.      */
  168.     cachedMeasuredItem_: null,
  169.  
  170.     /**
  171.      * The selection model to use.
  172.      * @type {cr.ui.ListSelectionModel}
  173.      */
  174.     get selectionModel() {
  175.       return this.selectionModel_;
  176.     },
  177.     set selectionModel(sm) {
  178.       var oldSm = this.selectionModel_;
  179.       if (oldSm == sm)
  180.         return;
  181.  
  182.       if (!this.boundHandleOnChange_) {
  183.         this.boundHandleOnChange_ = this.handleOnChange_.bind(this);
  184.         this.boundHandleLeadChange_ = this.handleLeadChange.bind(this);
  185.       }
  186.  
  187.       if (oldSm) {
  188.         oldSm.removeEventListener('change', this.boundHandleOnChange_);
  189.         oldSm.removeEventListener('leadIndexChange',
  190.                                   this.boundHandleLeadChange_);
  191.       }
  192.  
  193.       this.selectionModel_ = sm;
  194.       this.selectionController_ = this.createSelectionController(sm);
  195.  
  196.       if (sm) {
  197.         sm.addEventListener('change', this.boundHandleOnChange_);
  198.         sm.addEventListener('leadIndexChange', this.boundHandleLeadChange_);
  199.       }
  200.     },
  201.  
  202.     /**
  203.      * Whether or not the list auto-expands.
  204.      * @type {boolean}
  205.      */
  206.     get autoExpands() {
  207.       return this.autoExpands_;
  208.     },
  209.     set autoExpands(autoExpands) {
  210.       if (this.autoExpands_ == autoExpands)
  211.         return;
  212.       this.autoExpands_ = autoExpands;
  213.       this.redraw();
  214.     },
  215.  
  216.     /**
  217.      * Whether or not the rows on list have various heights.
  218.      * @type {boolean}
  219.      */
  220.     get fixedHeight() {
  221.       return this.fixedHeight_;
  222.     },
  223.     set fixedHeight(fixedHeight) {
  224.       if (this.fixedHeight_ == fixedHeight)
  225.         return;
  226.       this.fixedHeight_ = fixedHeight;
  227.       this.redraw();
  228.     },
  229.  
  230.     /**
  231.      * Convenience alias for selectionModel.selectedItem
  232.      * @type {*}
  233.      */
  234.     get selectedItem() {
  235.       var dataModel = this.dataModel;
  236.       if (dataModel) {
  237.         var index = this.selectionModel.selectedIndex;
  238.         if (index != -1)
  239.           return dataModel.item(index);
  240.       }
  241.       return null;
  242.     },
  243.     set selectedItem(selectedItem) {
  244.       var dataModel = this.dataModel;
  245.       if (dataModel) {
  246.         var index = this.dataModel.indexOf(selectedItem);
  247.         this.selectionModel.selectedIndex = index;
  248.       }
  249.     },
  250.  
  251.     /**
  252.      * Convenience alias for selectionModel.selectedItems
  253.      * @type {!Array.<*>}
  254.      */
  255.     get selectedItems() {
  256.       var indexes = this.selectionModel.selectedIndexes;
  257.       var dataModel = this.dataModel;
  258.       if (dataModel) {
  259.         return indexes.map(function(i) {
  260.           return dataModel.item(i);
  261.         });
  262.       }
  263.       return [];
  264.     },
  265.  
  266.     /**
  267.      * The HTML elements representing the items.
  268.      * @type {HTMLCollection}
  269.      */
  270.     get items() {
  271.       return Array.prototype.filter.call(this.children,
  272.                                          this.isItem, this);
  273.     },
  274.  
  275.     /**
  276.      * Returns true if the child is a list item. Subclasses may override this
  277.      * to filter out certain elements.
  278.      * @param {Node} child Child of the list.
  279.      * @return {boolean} True if a list item.
  280.      */
  281.     isItem: function(child) {
  282.       return child.nodeType == Node.ELEMENT_NODE &&
  283.              child != this.beforeFiller_ && child != this.afterFiller_;
  284.     },
  285.  
  286.     batchCount_: 0,
  287.  
  288.     /**
  289.      * When making a lot of updates to the list, the code could be wrapped in
  290.      * the startBatchUpdates and finishBatchUpdates to increase performance. Be
  291.      * sure that the code will not return without calling endBatchUpdates or the
  292.      * list will not be correctly updated.
  293.      */
  294.     startBatchUpdates: function() {
  295.       this.batchCount_++;
  296.     },
  297.  
  298.     /**
  299.      * See startBatchUpdates.
  300.      */
  301.     endBatchUpdates: function() {
  302.       this.batchCount_--;
  303.       if (this.batchCount_ == 0)
  304.         this.redraw();
  305.     },
  306.  
  307.     /**
  308.      * Initializes the element.
  309.      */
  310.     decorate: function() {
  311.       // Add fillers.
  312.       this.beforeFiller_ = this.ownerDocument.createElement('div');
  313.       this.afterFiller_ = this.ownerDocument.createElement('div');
  314.       this.beforeFiller_.className = 'spacer';
  315.       this.afterFiller_.className = 'spacer';
  316.       this.textContent = '';
  317.       this.appendChild(this.beforeFiller_);
  318.       this.appendChild(this.afterFiller_);
  319.  
  320.       var length = this.dataModel ? this.dataModel.length : 0;
  321.       this.selectionModel = new ListSelectionModel(length);
  322.  
  323.       this.addEventListener('dblclick', this.handleDoubleClick_);
  324.       this.addEventListener('mousedown', handleMouseDown);
  325.       this.addEventListener('dragstart', handleDragStart, true);
  326.       this.addEventListener('mouseup', this.handlePointerDownUp_);
  327.       this.addEventListener('keydown', this.handleKeyDown);
  328.       this.addEventListener('focus', this.handleElementFocus_, true);
  329.       this.addEventListener('blur', this.handleElementBlur_, true);
  330.       this.addEventListener('scroll', this.handleScroll.bind(this));
  331.       this.setAttribute('role', 'list');
  332.  
  333.       // Make list focusable
  334.       if (!this.hasAttribute('tabindex'))
  335.         this.tabIndex = 0;
  336.  
  337.       // Try to get an unique id prefix from the id of this element or the
  338.       // nearest ancestor with an id.
  339.       var element = this;
  340.       while (element && !element.id)
  341.         element = element.parentElement;
  342.       if (element && element.id)
  343.         this.uniqueIdPrefix_ = element.id;
  344.       else
  345.         this.uniqueIdPrefix_ = 'list';
  346.  
  347.       // The next id suffix to use when giving each item an unique id.
  348.       this.nextUniqueIdSuffix_ = 0;
  349.     },
  350.  
  351.     /**
  352.      * @param {cr.ui.ListItem=} item The list item to measure.
  353.      * @return {number} The height of the given item. If the fixed height on CSS
  354.      * is set by 'px', uses that value as height. Otherwise, measures the size.
  355.      * @private
  356.      */
  357.     measureItemHeight_: function(item) {
  358.       return this.measureItem(item).height;
  359.     },
  360.  
  361.     /**
  362.      * @return {number} The height of default item, measuring it if necessary.
  363.      * @private
  364.      */
  365.     getDefaultItemHeight_: function() {
  366.       return this.getDefaultItemSize_().height;
  367.     },
  368.  
  369.     /**
  370.      * @param {number} index The index of the item.
  371.      * @return {number} The height of the item, measuring it if necessary.
  372.      */
  373.     getItemHeightByIndex_: function(index) {
  374.       // If |this.fixedHeight_| is true, all the rows have same default height.
  375.       if (this.fixedHeight_)
  376.         return this.getDefaultItemHeight_();
  377.  
  378.       if (this.cachedItemHeights_[index])
  379.         return this.cachedItemHeights_[index];
  380.  
  381.       var item = this.getListItemByIndex(index);
  382.       if (item) {
  383.         var h = this.measureItemHeight_(item);
  384.         this.cachedItemHeights_[index] = h;
  385.         return h;
  386.       }
  387.       return this.getDefaultItemHeight_();
  388.     },
  389.  
  390.     /**
  391.      * @return {{height: number, width: number}} The height and width
  392.      *     of default item, measuring it if necessary.
  393.      * @private
  394.      */
  395.     getDefaultItemSize_: function() {
  396.       if (!this.measured_ || !this.measured_.height) {
  397.         this.measured_ = this.measureItem();
  398.       }
  399.       return this.measured_;
  400.     },
  401.  
  402.     /**
  403.      * Creates an item (dataModel.item(0)) and measures its height. The item is
  404.      * cached instead of creating a new one every time..
  405.      * @param {cr.ui.ListItem=} opt_item The list item to use to do the
  406.      *     measuring. If this is not provided an item will be created based on
  407.      *     the first value in the model.
  408.      * @return {{height: number, marginTop: number, marginBottom: number,
  409.      *     width: number, marginLeft: number, marginRight: number}}
  410.      *     The height and width of the item, taking
  411.      *     margins into account, and the top, bottom, left and right margins
  412.      *     themselves.
  413.      */
  414.     measureItem: function(opt_item) {
  415.       var dataModel = this.dataModel;
  416.       if (!dataModel || !dataModel.length) {
  417.         return {height: 0, marginTop: 0, marginBottom: 0,
  418.                 width: 0, marginLeft: 0, marginRight: 0};
  419.       }
  420.       var item = opt_item || this.cachedMeasuredItem_ ||
  421.           this.createItem(dataModel.item(0));
  422.       if (!opt_item) {
  423.         this.cachedMeasuredItem_ = item;
  424.         this.appendChild(item);
  425.       }
  426.  
  427.       var rect = item.getBoundingClientRect();
  428.       var cs = getComputedStyle(item);
  429.       var mt = parseFloat(cs.marginTop);
  430.       var mb = parseFloat(cs.marginBottom);
  431.       var ml = parseFloat(cs.marginLeft);
  432.       var mr = parseFloat(cs.marginRight);
  433.       var h = rect.height;
  434.       var w = rect.width;
  435.       var mh = 0;
  436.       var mv = 0;
  437.  
  438.       // Handle margin collapsing.
  439.       if (mt < 0 && mb < 0) {
  440.         mv = Math.min(mt, mb);
  441.       } else if (mt >= 0 && mb >= 0) {
  442.         mv = Math.max(mt, mb);
  443.       } else {
  444.         mv = mt + mb;
  445.       }
  446.       h += mv;
  447.  
  448.       if (ml < 0 && mr < 0) {
  449.         mh = Math.min(ml, mr);
  450.       } else if (ml >= 0 && mr >= 0) {
  451.         mh = Math.max(ml, mr);
  452.       } else {
  453.         mh = ml + mr;
  454.       }
  455.       w += mh;
  456.  
  457.       if (!opt_item)
  458.         this.removeChild(item);
  459.       return {
  460.           height: Math.max(0, h),
  461.           marginTop: mt, marginBottom: mb,
  462.           width: Math.max(0, w),
  463.           marginLeft: ml, marginRight: mr};
  464.     },
  465.  
  466.     /**
  467.      * Callback for the double click event.
  468.      * @param {Event} e The mouse event object.
  469.      * @private
  470.      */
  471.     handleDoubleClick_: function(e) {
  472.       if (this.disabled)
  473.         return;
  474.  
  475.       var target = /** @type {HTMLElement} */(e.target);
  476.  
  477.       var ancestor = this.getListItemAncestor(target);
  478.       var index = -1;
  479.       if (ancestor) {
  480.         index = this.getIndexOfListItem(ancestor);
  481.         this.activateItemAtIndex(index);
  482.       }
  483.  
  484.       var sm = this.selectionModel;
  485.       var indexSelected = sm.getIndexSelected(index);
  486.       if (!indexSelected)
  487.         this.handlePointerDownUp_(e);
  488.     },
  489.  
  490.     /**
  491.      * Callback for mousedown and mouseup events.
  492.      * @param {Event} e The mouse event object.
  493.      * @private
  494.      */
  495.     handlePointerDownUp_: function(e) {
  496.       if (this.disabled)
  497.         return;
  498.  
  499.       var target = /** @type {HTMLElement} */(e.target);
  500.  
  501.       // If the target was this element we need to make sure that the user did
  502.       // not click on a border or a scrollbar.
  503.       if (target == this) {
  504.         if (inViewport(target, e))
  505.           this.selectionController_.handlePointerDownUp(e, -1);
  506.         return;
  507.       }
  508.  
  509.       target = this.getListItemAncestor(target);
  510.  
  511.       var index = this.getIndexOfListItem(target);
  512.       this.selectionController_.handlePointerDownUp(e, index);
  513.     },
  514.  
  515.     /**
  516.      * Called when an element in the list is focused. Marks the list as having
  517.      * a focused element, and dispatches an event if it didn't have focus.
  518.      * @param {Event} e The focus event.
  519.      * @private
  520.      */
  521.     handleElementFocus_: function(e) {
  522.       if (!this.hasElementFocus)
  523.         this.hasElementFocus = true;
  524.     },
  525.  
  526.     /**
  527.      * Called when an element in the list is blurred. If focus moves outside
  528.      * the list, marks the list as no longer having focus and dispatches an
  529.      * event.
  530.      * @param {Event} e The blur event.
  531.      * @private
  532.      * @suppress {checkTypes}
  533.      * TODO(dbeam): remove suppression when the extern
  534.      * Node.prototype.contains() will be fixed.
  535.      */
  536.     handleElementBlur_: function(e) {
  537.       if (!this.contains(e.relatedTarget))
  538.         this.hasElementFocus = false;
  539.     },
  540.  
  541.     /**
  542.      * Returns the list item element containing the given element, or null if
  543.      * it doesn't belong to any list item element.
  544.      * @param {HTMLElement} element The element.
  545.      * @return {HTMLLIElement} The list item containing |element|, or null.
  546.      */
  547.     getListItemAncestor: function(element) {
  548.       var container = element;
  549.       while (container && container.parentNode != this) {
  550.         container = container.parentNode;
  551.       }
  552.       return container && assertInstanceof(container, HTMLLIElement);
  553.     },
  554.  
  555.     /**
  556.      * Handle a keydown event.
  557.      * @param {Event} e The keydown event.
  558.      */
  559.     handleKeyDown: function(e) {
  560.       if (!this.disabled)
  561.         this.selectionController_.handleKeyDown(e);
  562.     },
  563.  
  564.     /**
  565.      * Handle a scroll event.
  566.      * @param {Event} e The scroll event.
  567.      */
  568.     handleScroll: function(e) {
  569.       requestAnimationFrame(this.redraw.bind(this));
  570.     },
  571.  
  572.     /**
  573.      * Callback from the selection model. We dispatch {@code change} events
  574.      * when the selection changes.
  575.      * @param {!Event} ce Event with change info.
  576.      * @private
  577.      */
  578.     handleOnChange_: function(ce) {
  579.       ce.changes.forEach(function(change) {
  580.         var listItem = this.getListItemByIndex(change.index);
  581.         if (listItem) {
  582.           listItem.selected = change.selected;
  583.           if (change.selected) {
  584.             listItem.setAttribute('aria-posinset', change.index + 1);
  585.             listItem.setAttribute('aria-setsize', this.dataModel.length);
  586.             this.setAttribute('aria-activedescendant', listItem.id);
  587.           } else {
  588.             listItem.removeAttribute('aria-posinset');
  589.             listItem.removeAttribute('aria-setsize');
  590.           }
  591.         }
  592.       }, this);
  593.  
  594.       cr.dispatchSimpleEvent(this, 'change');
  595.     },
  596.  
  597.     /**
  598.      * Handles a change of the lead item from the selection model.
  599.      * @param {Event} e The property change event.
  600.      * @protected
  601.      */
  602.     handleLeadChange: function(e) {
  603.       var element;
  604.       if (e.oldValue != -1) {
  605.         if ((element = this.getListItemByIndex(e.oldValue)))
  606.           element.lead = false;
  607.       }
  608.  
  609.       if (e.newValue != -1) {
  610.         if ((element = this.getListItemByIndex(e.newValue)))
  611.           element.lead = true;
  612.         if (e.oldValue != e.newValue) {
  613.           this.scrollIndexIntoView(e.newValue);
  614.           // If the lead item has a different height than other items, then we
  615.           // may run into a problem that requires a second attempt to scroll
  616.           // it into view. The first scroll attempt will trigger a redraw,
  617.           // which will clear out the list and repopulate it with new items.
  618.           // During the redraw, the list may shrink temporarily, which if the
  619.           // lead item is the last item, will move the scrollTop up since it
  620.           // cannot extend beyond the end of the list. (Sadly, being scrolled to
  621.           // the bottom of the list is not "sticky.") So, we set a timeout to
  622.           // rescroll the list after this all gets sorted out. This is perhaps
  623.           // not the most elegant solution, but no others seem obvious.
  624.           var self = this;
  625.           window.setTimeout(function() {
  626.             self.scrollIndexIntoView(e.newValue);
  627.           }, 0);
  628.         }
  629.       }
  630.     },
  631.  
  632.     /**
  633.      * This handles data model 'permuted' event.
  634.      * this event is dispatched as a part of sort or splice.
  635.      * We need to
  636.      *  - adjust the cache.
  637.      *  - adjust selection.
  638.      *  - redraw. (called in this.endBatchUpdates())
  639.      *  It is important that the cache adjustment happens before selection model
  640.      *  adjustments.
  641.      * @param {Event} e The 'permuted' event.
  642.      */
  643.     handleDataModelPermuted_: function(e) {
  644.       var newCachedItems = {};
  645.       for (var index in this.cachedItems_) {
  646.         if (e.permutation[index] != -1) {
  647.           var newIndex = e.permutation[index];
  648.           newCachedItems[newIndex] = this.cachedItems_[index];
  649.           newCachedItems[newIndex].listIndex = newIndex;
  650.         }
  651.       }
  652.       this.cachedItems_ = newCachedItems;
  653.       this.pinnedItem_ = null;
  654.  
  655.       var newCachedItemHeights = {};
  656.       for (var index in this.cachedItemHeights_) {
  657.         if (e.permutation[index] != -1) {
  658.           newCachedItemHeights[e.permutation[index]] =
  659.               this.cachedItemHeights_[index];
  660.         }
  661.       }
  662.       this.cachedItemHeights_ = newCachedItemHeights;
  663.  
  664.       this.startBatchUpdates();
  665.  
  666.       var sm = this.selectionModel;
  667.       sm.adjustLength(e.newLength);
  668.       sm.adjustToReordering(e.permutation);
  669.  
  670.       this.endBatchUpdates();
  671.     },
  672.  
  673.     handleDataModelChange_: function(e) {
  674.       delete this.cachedItems_[e.index];
  675.       delete this.cachedItemHeights_[e.index];
  676.       this.cachedMeasuredItem_ = null;
  677.  
  678.       if (e.index >= this.firstIndex_ &&
  679.           (e.index < this.lastIndex_ || this.remainingSpace_)) {
  680.         this.redraw();
  681.       }
  682.     },
  683.  
  684.     /**
  685.      * @param {number} index The index of the item.
  686.      * @return {number} The top position of the item inside the list.
  687.      */
  688.     getItemTop: function(index) {
  689.       if (this.fixedHeight_) {
  690.         var itemHeight = this.getDefaultItemHeight_();
  691.         return index * itemHeight;
  692.       } else {
  693.         this.ensureAllItemSizesInCache();
  694.         var top = 0;
  695.         for (var i = 0; i < index; i++) {
  696.           top += this.getItemHeightByIndex_(i);
  697.         }
  698.         return top;
  699.       }
  700.     },
  701.  
  702.     /**
  703.      * @param {number} index The index of the item.
  704.      * @return {number} The row of the item. May vary in the case
  705.      *     of multiple columns.
  706.      */
  707.     getItemRow: function(index) {
  708.       return index;
  709.     },
  710.  
  711.     /**
  712.      * @param {number} row The row.
  713.      * @return {number} The index of the first item in the row.
  714.      */
  715.     getFirstItemInRow: function(row) {
  716.       return row;
  717.     },
  718.  
  719.     /**
  720.      * Ensures that a given index is inside the viewport.
  721.      * @param {number} index The index of the item to scroll into view.
  722.      * @return {boolean} Whether any scrolling was needed.
  723.      */
  724.     scrollIndexIntoView: function(index) {
  725.       var dataModel = this.dataModel;
  726.       if (!dataModel || index < 0 || index >= dataModel.length)
  727.         return false;
  728.  
  729.       var itemHeight = this.getItemHeightByIndex_(index);
  730.       var scrollTop = this.scrollTop;
  731.       var top = this.getItemTop(index);
  732.       var clientHeight = this.clientHeight;
  733.  
  734.       var cs = getComputedStyle(this);
  735.       var paddingY = parseInt(cs.paddingTop, 10) +
  736.                      parseInt(cs.paddingBottom, 10);
  737.       var availableHeight = clientHeight - paddingY;
  738.  
  739.       var self = this;
  740.       // Function to adjust the tops of viewport and row.
  741.       function scrollToAdjustTop() {
  742.           self.scrollTop = top;
  743.           return true;
  744.       };
  745.       // Function to adjust the bottoms of viewport and row.
  746.       function scrollToAdjustBottom() {
  747.           self.scrollTop = top + itemHeight - availableHeight;
  748.           return true;
  749.       };
  750.  
  751.       // Check if the entire of given indexed row can be shown in the viewport.
  752.       if (itemHeight <= availableHeight) {
  753.         if (top < scrollTop)
  754.           return scrollToAdjustTop();
  755.         if (scrollTop + availableHeight < top + itemHeight)
  756.           return scrollToAdjustBottom();
  757.       } else {
  758.         if (scrollTop < top)
  759.           return scrollToAdjustTop();
  760.         if (top + itemHeight < scrollTop + availableHeight)
  761.           return scrollToAdjustBottom();
  762.       }
  763.       return false;
  764.     },
  765.  
  766.     /**
  767.      * @return {!ClientRect} The rect to use for the context menu.
  768.      */
  769.     getRectForContextMenu: function() {
  770.       // TODO(arv): Add trait support so we can share more code between trees
  771.       // and lists.
  772.       var index = this.selectionModel.selectedIndex;
  773.       var el = this.getListItemByIndex(index);
  774.       if (el)
  775.         return el.getBoundingClientRect();
  776.       return this.getBoundingClientRect();
  777.     },
  778.  
  779.     /**
  780.      * Takes a value from the data model and finds the associated list item.
  781.      * @param {*} value The value in the data model that we want to get the list
  782.      *     item for.
  783.      * @return {cr.ui.ListItem} The first found list item or null if not found.
  784.      */
  785.     getListItem: function(value) {
  786.       var dataModel = this.dataModel;
  787.       if (dataModel) {
  788.         var index = dataModel.indexOf(value);
  789.         return this.getListItemByIndex(index);
  790.       }
  791.       return null;
  792.     },
  793.  
  794.     /**
  795.      * Find the list item element at the given index.
  796.      * @param {number} index The index of the list item to get.
  797.      * @return {cr.ui.ListItem} The found list item or null if not found.
  798.      */
  799.     getListItemByIndex: function(index) {
  800.       return this.cachedItems_[index] || null;
  801.     },
  802.  
  803.     /**
  804.      * Find the index of the given list item element.
  805.      * @param {HTMLLIElement} item The list item to get the index of.
  806.      * @return {number} The index of the list item, or -1 if not found.
  807.      */
  808.     getIndexOfListItem: function(item) {
  809.       var index = item.listIndex;
  810.       if (this.cachedItems_[index] == item) {
  811.         return index;
  812.       }
  813.       return -1;
  814.     },
  815.  
  816.     /**
  817.      * Creates a new list item.
  818.      * @param {*} value The value to use for the item.
  819.      * @return {!cr.ui.ListItem} The newly created list item.
  820.      */
  821.     createItem: function(value) {
  822.       var item = new this.itemConstructor_(value);
  823.       item.label = value;
  824.       item.id = this.uniqueIdPrefix_ + '-' + this.nextUniqueIdSuffix_++;
  825.       if (typeof item.decorate == 'function')
  826.         item.decorate();
  827.       return item;
  828.     },
  829.  
  830.     /**
  831.      * Creates the selection controller to use internally.
  832.      * @param {cr.ui.ListSelectionModel} sm The underlying selection model.
  833.      * @return {!cr.ui.ListSelectionController} The newly created selection
  834.      *     controller.
  835.      */
  836.     createSelectionController: function(sm) {
  837.       return new ListSelectionController(sm);
  838.     },
  839.  
  840.     /**
  841.      * Return the heights (in pixels) of the top of the given item index within
  842.      * the list, and the height of the given item itself, accounting for the
  843.      * possibility that the lead item may be a different height.
  844.      * @param {number} index The index to find the top height of.
  845.      * @return {{top: number, height: number}} The heights for the given index.
  846.      * @private
  847.      */
  848.     getHeightsForIndex_: function(index) {
  849.       var itemHeight = this.getItemHeightByIndex_(index);
  850.       var top = this.getItemTop(index);
  851.       return {top: top, height: itemHeight};
  852.     },
  853.  
  854.     /**
  855.      * Find the index of the list item containing the given y offset (measured
  856.      * in pixels from the top) within the list. In the case of multiple columns,
  857.      * returns the first index in the row.
  858.      * @param {number} offset The y offset in pixels to get the index of.
  859.      * @return {number} The index of the list item. Returns the list size if
  860.      *     given offset exceeds the height of list.
  861.      * @private
  862.      */
  863.     getIndexForListOffset_: function(offset) {
  864.       var itemHeight = this.getDefaultItemHeight_();
  865.       if (!itemHeight)
  866.         return this.dataModel.length;
  867.  
  868.       if (this.fixedHeight_)
  869.         return this.getFirstItemInRow(Math.floor(offset / itemHeight));
  870.  
  871.       // If offset exceeds the height of list.
  872.       var lastHeight = 0;
  873.       if (this.dataModel.length) {
  874.         var h = this.getHeightsForIndex_(this.dataModel.length - 1);
  875.         lastHeight = h.top + h.height;
  876.       }
  877.       if (lastHeight < offset)
  878.         return this.dataModel.length;
  879.  
  880.       // Estimates index.
  881.       var estimatedIndex = Math.min(Math.floor(offset / itemHeight),
  882.                                     this.dataModel.length - 1);
  883.       var isIncrementing = this.getItemTop(estimatedIndex) < offset;
  884.  
  885.       // Searchs the correct index.
  886.       do {
  887.         var heights = this.getHeightsForIndex_(estimatedIndex);
  888.         var top = heights.top;
  889.         var height = heights.height;
  890.  
  891.         if (top <= offset && offset <= (top + height))
  892.           break;
  893.  
  894.         isIncrementing ? ++estimatedIndex : --estimatedIndex;
  895.       } while (0 < estimatedIndex && estimatedIndex < this.dataModel.length);
  896.  
  897.       return estimatedIndex;
  898.     },
  899.  
  900.     /**
  901.      * Return the number of items that occupy the range of heights between the
  902.      * top of the start item and the end offset.
  903.      * @param {number} startIndex The index of the first visible item.
  904.      * @param {number} endOffset The y offset in pixels of the end of the list.
  905.      * @return {number} The number of list items visible.
  906.      * @private
  907.      */
  908.     countItemsInRange_: function(startIndex, endOffset) {
  909.       var endIndex = this.getIndexForListOffset_(endOffset);
  910.       return endIndex - startIndex + 1;
  911.     },
  912.  
  913.     /**
  914.      * Calculates the number of items fitting in the given viewport.
  915.      * @param {number} scrollTop The scroll top position.
  916.      * @param {number} clientHeight The height of viewport.
  917.      * @return {{first: number, length: number, last: number}} The index of
  918.      *     first item in view port, The number of items, The item past the last.
  919.      */
  920.     getItemsInViewPort: function(scrollTop, clientHeight) {
  921.       if (this.autoExpands_) {
  922.         return {
  923.           first: 0,
  924.           length: this.dataModel.length,
  925.           last: this.dataModel.length};
  926.       } else {
  927.         var firstIndex = this.getIndexForListOffset_(scrollTop);
  928.         var lastIndex = this.getIndexForListOffset_(scrollTop + clientHeight);
  929.  
  930.         return {
  931.           first: firstIndex,
  932.           length: lastIndex - firstIndex + 1,
  933.           last: lastIndex + 1};
  934.       }
  935.     },
  936.  
  937.     /**
  938.      * Merges list items currently existing in the list with items in the range
  939.      * [firstIndex, lastIndex). Removes or adds items if needed.
  940.      * Doesn't delete {@code this.pinnedItem_} if it is present (instead hides
  941.      * it if it is out of the range).
  942.      * @param {number} firstIndex The index of first item, inclusively.
  943.      * @param {number} lastIndex The index of last item, exclusively.
  944.      */
  945.     mergeItems: function(firstIndex, lastIndex) {
  946.       var self = this;
  947.       var dataModel = this.dataModel;
  948.       var currentIndex = firstIndex;
  949.  
  950.       function insert() {
  951.         var dataItem = dataModel.item(currentIndex);
  952.         var newItem = self.cachedItems_[currentIndex] ||
  953.             self.createItem(dataItem);
  954.         newItem.listIndex = currentIndex;
  955.         self.cachedItems_[currentIndex] = newItem;
  956.         self.insertBefore(newItem, item);
  957.         currentIndex++;
  958.       }
  959.  
  960.       function remove() {
  961.         var next = item.nextSibling;
  962.         if (item != self.pinnedItem_)
  963.           self.removeChild(item);
  964.         item = next;
  965.       }
  966.  
  967.       for (var item = this.beforeFiller_.nextSibling;
  968.            item != this.afterFiller_ && currentIndex < lastIndex;) {
  969.         if (!this.isItem(item)) {
  970.           item = item.nextSibling;
  971.           continue;
  972.         }
  973.  
  974.         var index = item.listIndex;
  975.         if (this.cachedItems_[index] != item || index < currentIndex) {
  976.           remove();
  977.         } else if (index == currentIndex) {
  978.           this.cachedItems_[currentIndex] = item;
  979.           item = item.nextSibling;
  980.           currentIndex++;
  981.         } else {  // index > currentIndex
  982.           insert();
  983.         }
  984.       }
  985.  
  986.       while (item != this.afterFiller_) {
  987.         if (this.isItem(item))
  988.           remove();
  989.         else
  990.           item = item.nextSibling;
  991.       }
  992.  
  993.       if (this.pinnedItem_) {
  994.         var index = this.pinnedItem_.listIndex;
  995.         this.pinnedItem_.hidden = index < firstIndex || index >= lastIndex;
  996.         this.cachedItems_[index] = this.pinnedItem_;
  997.         if (index >= lastIndex)
  998.           item = this.pinnedItem_;  // Insert new items before this one.
  999.       }
  1000.  
  1001.       while (currentIndex < lastIndex)
  1002.         insert();
  1003.     },
  1004.  
  1005.     /**
  1006.      * Ensures that all the item sizes in the list have been already cached.
  1007.      */
  1008.     ensureAllItemSizesInCache: function() {
  1009.       var measuringIndexes = [];
  1010.       var isElementAppended = [];
  1011.       for (var y = 0; y < this.dataModel.length; y++) {
  1012.         if (!this.cachedItemHeights_[y]) {
  1013.           measuringIndexes.push(y);
  1014.           isElementAppended.push(false);
  1015.         }
  1016.       }
  1017.  
  1018.       var measuringItems = [];
  1019.       // Adds temporary elements.
  1020.       for (var y = 0; y < measuringIndexes.length; y++) {
  1021.         var index = measuringIndexes[y];
  1022.         var dataItem = this.dataModel.item(index);
  1023.         var listItem = this.cachedItems_[index] || this.createItem(dataItem);
  1024.         listItem.listIndex = index;
  1025.  
  1026.         // If |listItems| is not on the list, apppends it to the list and sets
  1027.         // the flag.
  1028.         if (!listItem.parentNode) {
  1029.           this.appendChild(listItem);
  1030.           isElementAppended[y] = true;
  1031.         }
  1032.  
  1033.         this.cachedItems_[index] = listItem;
  1034.         measuringItems.push(listItem);
  1035.       }
  1036.  
  1037.       // All mesurings must be placed after adding all the elements, to prevent
  1038.       // performance reducing.
  1039.       for (var y = 0; y < measuringIndexes.length; y++) {
  1040.         var index = measuringIndexes[y];
  1041.         this.cachedItemHeights_[index] =
  1042.             this.measureItemHeight_(measuringItems[y]);
  1043.       }
  1044.  
  1045.       // Removes all the temprary elements.
  1046.       for (var y = 0; y < measuringIndexes.length; y++) {
  1047.         // If the list item has been appended above, removes it.
  1048.         if (isElementAppended[y])
  1049.           this.removeChild(measuringItems[y]);
  1050.       }
  1051.     },
  1052.  
  1053.     /**
  1054.      * Returns the height of after filler in the list.
  1055.      * @param {number} lastIndex The index of item past the last in viewport.
  1056.      * @return {number} The height of after filler.
  1057.      */
  1058.     getAfterFillerHeight: function(lastIndex) {
  1059.       if (this.fixedHeight_) {
  1060.         var itemHeight = this.getDefaultItemHeight_();
  1061.         return (this.dataModel.length - lastIndex) * itemHeight;
  1062.       }
  1063.  
  1064.       var height = 0;
  1065.       for (var i = lastIndex; i < this.dataModel.length; i++)
  1066.         height += this.getItemHeightByIndex_(i);
  1067.       return height;
  1068.     },
  1069.  
  1070.     /**
  1071.      * Redraws the viewport.
  1072.      */
  1073.     redraw: function() {
  1074.       if (this.batchCount_ != 0)
  1075.         return;
  1076.  
  1077.       var dataModel = this.dataModel;
  1078.       if (!dataModel || !this.autoExpands_ && this.clientHeight == 0) {
  1079.         this.cachedItems_ = {};
  1080.         this.firstIndex_ = 0;
  1081.         this.lastIndex_ = 0;
  1082.         this.remainingSpace_ = this.clientHeight != 0;
  1083.         this.mergeItems(0, 0);
  1084.         return;
  1085.       }
  1086.  
  1087.       // Save the previous positions before any manipulation of elements.
  1088.       var scrollTop = this.scrollTop;
  1089.       var clientHeight = this.clientHeight;
  1090.  
  1091.       // Store all the item sizes into the cache in advance, to prevent
  1092.       // interleave measuring with mutating dom.
  1093.       if (!this.fixedHeight_)
  1094.         this.ensureAllItemSizesInCache();
  1095.  
  1096.       var autoExpands = this.autoExpands_;
  1097.  
  1098.       var itemsInViewPort = this.getItemsInViewPort(scrollTop, clientHeight);
  1099.       // Draws the hidden rows just above/below the viewport to prevent
  1100.       // flashing in scroll.
  1101.       var firstIndex = Math.max(
  1102.           0,
  1103.           Math.min(dataModel.length - 1, itemsInViewPort.first - 1));
  1104.       var lastIndex = Math.min(itemsInViewPort.last + 1, dataModel.length);
  1105.  
  1106.       var beforeFillerHeight =
  1107.           this.autoExpands ? 0 : this.getItemTop(firstIndex);
  1108.       var afterFillerHeight =
  1109.           this.autoExpands ? 0 : this.getAfterFillerHeight(lastIndex);
  1110.  
  1111.       this.beforeFiller_.style.height = beforeFillerHeight + 'px';
  1112.  
  1113.       var sm = this.selectionModel;
  1114.       var leadIndex = sm.leadIndex;
  1115.  
  1116.       // If the pinned item is hidden and it is not the lead item, then remove
  1117.       // it from cache. Note, that we restore the hidden status to false, since
  1118.       // the item is still in cache, and may be reused.
  1119.       if (this.pinnedItem_ &&
  1120.           this.pinnedItem_ != this.cachedItems_[leadIndex]) {
  1121.         if (this.pinnedItem_.hidden) {
  1122.           this.removeChild(this.pinnedItem_);
  1123.           this.pinnedItem_.hidden = false;
  1124.         }
  1125.         this.pinnedItem_ = undefined;
  1126.       }
  1127.  
  1128.       this.mergeItems(firstIndex, lastIndex);
  1129.  
  1130.       if (!this.pinnedItem_ && this.cachedItems_[leadIndex] &&
  1131.           this.cachedItems_[leadIndex].parentNode == this) {
  1132.         this.pinnedItem_ = this.cachedItems_[leadIndex];
  1133.       }
  1134.  
  1135.       this.afterFiller_.style.height = afterFillerHeight + 'px';
  1136.  
  1137.       // Restores the number of pixels scrolled, since it might be changed while
  1138.       // DOM operations.
  1139.       this.scrollTop = scrollTop;
  1140.  
  1141.       // We don't set the lead or selected properties until after adding all
  1142.       // items, in case they force relayout in response to these events.
  1143.       if (leadIndex != -1 && this.cachedItems_[leadIndex])
  1144.         this.cachedItems_[leadIndex].lead = true;
  1145.       for (var y = firstIndex; y < lastIndex; y++) {
  1146.         if (sm.getIndexSelected(y) != this.cachedItems_[y].selected)
  1147.           this.cachedItems_[y].selected = !this.cachedItems_[y].selected;
  1148.       }
  1149.  
  1150.       this.firstIndex_ = firstIndex;
  1151.       this.lastIndex_ = lastIndex;
  1152.  
  1153.       this.remainingSpace_ = itemsInViewPort.last > dataModel.length;
  1154.  
  1155.       // Mesurings must be placed after adding all the elements, to prevent
  1156.       // performance reducing.
  1157.       if (!this.fixedHeight_) {
  1158.         for (var y = firstIndex; y < lastIndex; y++) {
  1159.           this.cachedItemHeights_[y] =
  1160.               this.measureItemHeight_(this.cachedItems_[y]);
  1161.         }
  1162.       }
  1163.     },
  1164.  
  1165.     /**
  1166.      * Restore the lead item that is present in the list but may be updated
  1167.      * in the data model (supposed to be used inside a batch update). Usually
  1168.      * such an item would be recreated in the redraw method. If reinsertion
  1169.      * is undesirable (for instance to prevent losing focus) the item may be
  1170.      * updated and restored. Assumed the listItem relates to the same data item
  1171.      * as the lead item in the begin of the batch update.
  1172.      *
  1173.      * @param {cr.ui.ListItem} leadItem Already existing lead item.
  1174.      */
  1175.     restoreLeadItem: function(leadItem) {
  1176.       delete this.cachedItems_[leadItem.listIndex];
  1177.  
  1178.       leadItem.listIndex = this.selectionModel.leadIndex;
  1179.       this.pinnedItem_ = this.cachedItems_[leadItem.listIndex] = leadItem;
  1180.     },
  1181.  
  1182.     /**
  1183.      * Invalidates list by removing cached items.
  1184.      */
  1185.     invalidate: function() {
  1186.       this.cachedItems_ = {};
  1187.       this.cachedItemSized_ = {};
  1188.     },
  1189.  
  1190.     /**
  1191.      * Redraws a single item.
  1192.      * @param {number} index The row index to redraw.
  1193.      */
  1194.     redrawItem: function(index) {
  1195.       if (index >= this.firstIndex_ &&
  1196.           (index < this.lastIndex_ || this.remainingSpace_)) {
  1197.         delete this.cachedItems_[index];
  1198.         this.redraw();
  1199.       }
  1200.     },
  1201.  
  1202.     /**
  1203.      * Called when a list item is activated, currently only by a double click
  1204.      * event.
  1205.      * @param {number} index The index of the activated item.
  1206.      */
  1207.     activateItemAtIndex: function(index) {
  1208.     },
  1209.  
  1210.     /**
  1211.      * Returns a ListItem for the leadIndex. If the item isn't present in the
  1212.      * list creates it and inserts to the list (may be invisible if it's out of
  1213.      * the visible range).
  1214.      *
  1215.      * Item returned from this method won't be removed until it remains a lead
  1216.      * item or till the data model changes (unlike other items that could be
  1217.      * removed when they go out of the visible range).
  1218.      *
  1219.      * @return {cr.ui.ListItem} The lead item for the list.
  1220.      */
  1221.     ensureLeadItemExists: function() {
  1222.       var index = this.selectionModel.leadIndex;
  1223.       if (index < 0)
  1224.         return null;
  1225.       var cachedItems = this.cachedItems_ || {};
  1226.  
  1227.       var item = cachedItems[index] ||
  1228.                  this.createItem(this.dataModel.item(index));
  1229.       if (this.pinnedItem_ != item && this.pinnedItem_ &&
  1230.           this.pinnedItem_.hidden) {
  1231.         this.removeChild(this.pinnedItem_);
  1232.       }
  1233.       this.pinnedItem_ = item;
  1234.       cachedItems[index] = item;
  1235.       item.listIndex = index;
  1236.       if (item.parentNode == this)
  1237.         return item;
  1238.  
  1239.       if (this.batchCount_ != 0)
  1240.         item.hidden = true;
  1241.  
  1242.       // Item will get to the right place in redraw. Choose place to insert
  1243.       // reducing items reinsertion.
  1244.       if (index <= this.firstIndex_)
  1245.         this.insertBefore(item, this.beforeFiller_.nextSibling);
  1246.       else
  1247.         this.insertBefore(item, this.afterFiller_);
  1248.       this.redraw();
  1249.       return item;
  1250.     },
  1251.  
  1252.     /**
  1253.      * Starts drag selection by reacting 'dragstart' event.
  1254.      * @param {Event} event Event of dragstart.
  1255.      */
  1256.     startDragSelection: function(event) {
  1257.       event.preventDefault();
  1258.       var border = document.createElement('div');
  1259.       border.className = 'drag-selection-border';
  1260.       var rect = this.getBoundingClientRect();
  1261.       var startX = event.clientX - rect.left + this.scrollLeft;
  1262.       var startY = event.clientY - rect.top + this.scrollTop;
  1263.       border.style.left = startX + 'px';
  1264.       border.style.top = startY + 'px';
  1265.       var onMouseMove = function(event) {
  1266.         var inRect = this.getBoundingClientRect();
  1267.         var x = event.clientX - inRect.left + this.scrollLeft;
  1268.         var y = event.clientY - inRect.top + this.scrollTop;
  1269.         border.style.left = Math.min(startX, x) + 'px';
  1270.         border.style.top = Math.min(startY, y) + 'px';
  1271.         border.style.width = Math.abs(startX - x) + 'px';
  1272.         border.style.height = Math.abs(startY - y) + 'px';
  1273.       }.bind(this);
  1274.       var onMouseUp = function() {
  1275.         this.removeChild(border);
  1276.         document.removeEventListener('mousemove', onMouseMove, true);
  1277.         document.removeEventListener('mouseup', onMouseUp, true);
  1278.       }.bind(this);
  1279.       document.addEventListener('mousemove', onMouseMove, true);
  1280.       document.addEventListener('mouseup', onMouseUp, true);
  1281.       this.appendChild(border);
  1282.     },
  1283.   };
  1284.  
  1285.   cr.defineProperty(List, 'disabled', cr.PropertyKind.BOOL_ATTR);
  1286.  
  1287.   /**
  1288.    * Whether the list or one of its descendents has focus. This is necessary
  1289.    * because list items can contain controls that can be focused, and for some
  1290.    * purposes (e.g., styling), the list can still be conceptually focused at
  1291.    * that point even though it doesn't actually have the page focus.
  1292.    */
  1293.   cr.defineProperty(List, 'hasElementFocus', cr.PropertyKind.BOOL_ATTR);
  1294.  
  1295.   /**
  1296.    * Mousedown event handler.
  1297.    * @this {cr.ui.List}
  1298.    * @param {Event} e The mouse event object.
  1299.    */
  1300.   function handleMouseDown(e) {
  1301.     e.target = /** @type {!HTMLElement} */(e.target);
  1302.     var listItem = this.getListItemAncestor(e.target);
  1303.     var wasSelected = listItem && listItem.selected;
  1304.     this.handlePointerDownUp_(e);
  1305.  
  1306.     if (e.defaultPrevented || e.button != 0)
  1307.       return;
  1308.  
  1309.     // The following hack is required only if the listItem gets selected.
  1310.     if (!listItem || wasSelected || !listItem.selected)
  1311.       return;
  1312.  
  1313.     // If non-focusable area in a list item is clicked and the item still
  1314.     // contains the focused element, the item did a special focus handling
  1315.     // [1] and we should not focus on the list.
  1316.     //
  1317.     // [1] For example, clicking non-focusable area gives focus on the first
  1318.     // form control in the item.
  1319.     if (!containsFocusableElement(e.target, listItem) &&
  1320.         listItem.contains(listItem.ownerDocument.activeElement)) {
  1321.       e.preventDefault();
  1322.     }
  1323.   }
  1324.  
  1325.   /**
  1326.    * Dragstart event handler.
  1327.    * If there is an item at starting position of drag operation and the item
  1328.    * is not selected, select it.
  1329.    * @this {cr.ui.List}
  1330.    * @param {Event} e The event object for 'dragstart'.
  1331.    */
  1332.   function handleDragStart(e) {
  1333.     e = /** @type {MouseEvent} */(e);
  1334.     var element = e.target.ownerDocument.elementFromPoint(e.clientX, e.clientY);
  1335.     var listItem = this.getListItemAncestor(element);
  1336.     if (!listItem)
  1337.       return;
  1338.  
  1339.     var index = this.getIndexOfListItem(listItem);
  1340.     if (index == -1)
  1341.       return;
  1342.  
  1343.     var isAlreadySelected = this.selectionModel_.getIndexSelected(index);
  1344.     if (!isAlreadySelected)
  1345.       this.selectionModel_.selectedIndex = index;
  1346.   }
  1347.  
  1348.   /**
  1349.    * Check if |start| or its ancestor under |root| is focusable.
  1350.    * This is a helper for handleMouseDown.
  1351.    * @param {!Element} start An element which we start to check.
  1352.    * @param {!Element} root An element which we finish to check.
  1353.    * @return {boolean} True if we found a focusable element.
  1354.    */
  1355.   function containsFocusableElement(start, root) {
  1356.     for (var element = start; element && element != root;
  1357.         element = element.parentElement) {
  1358.       if (element.tabIndex >= 0 && !element.disabled)
  1359.         return true;
  1360.     }
  1361.     return false;
  1362.   }
  1363.  
  1364.   return {
  1365.     List: List
  1366.   };
  1367. });
  1368.